/*
 * Copyright 2008-2023 dexian.vip. All rights reserved.
 * Support: http://www.dexian.vip
 * License: http://www.dexian.vip/license
 */

package vip.dexian.core.config.redis;

import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.databind.jsontype.impl.LaissezFaireSubTypeValidator;
import com.fasterxml.jackson.databind.ser.FilterProvider;
import com.fasterxml.jackson.databind.ser.impl.SimpleBeanPropertyFilter;
import com.fasterxml.jackson.databind.ser.impl.SimpleFilterProvider;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections4.MapUtils;
import org.springframework.cache.Cache;
import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.CachingConfigurer;
import org.springframework.cache.interceptor.CacheErrorHandler;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
import org.springframework.data.redis.core.HashOperations;
import org.springframework.data.redis.core.ListOperations;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.SetOperations;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.data.redis.core.ZSetOperations;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializationContext;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;

import java.time.Duration;
import java.util.HashMap;
import java.util.Map;
import java.util.TimeZone;

/**
 * Redis配置
 *
 * @author 挺好的 2023年05月30日 10:45
 */
@Configuration
@Slf4j
public class RedisConfig implements CachingConfigurer {

    /**
     * 缓存
     */
    @Resource (name = "redisCacheProperties")
    private RedisCacheProperties cacheProperties;

    /**
     * 默认过期时间
     */
    private static final long DEFAULT_TTL = 3600L;

    /**
     * lettuce 工厂
     */
    @Resource
    private LettuceConnectionFactory lettuceConnectionFactory;

    /**
     * 缓存管理器 使用Lettuce
     *
     * @return {@link org.springframework.cache.CacheManager}
     */
    @Override
    @Bean
    public CacheManager cacheManager () {
        // 自定义缓存配置，具体配置在 application-redis-cache.yml
        Map <String, RedisCacheConfiguration> redisCacheConfigurationMap = this.getRedisCacheConfigurationMap();

        //key序列化方式
        RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()

                .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(this.keySerializer()))
                //value序列化方式
                .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(this.valueSerializer()))

                .entryTtl(Duration.ofSeconds(DEFAULT_TTL))

                //禁止缓存空值
                .disableCachingNullValues();


        RedisCacheManager.RedisCacheManagerBuilder builder = RedisCacheManager.RedisCacheManagerBuilder

                .fromConnectionFactory(this.lettuceConnectionFactory)

                .cacheDefaults(config)

                .withInitialCacheConfigurations(redisCacheConfigurationMap)

                .transactionAware();

        return builder.build();
    }


    /**
     * RedisTemplate配置 在单独使用redisTemplate的时候 重新定义序列化方式
     *
     * @param lettuceConnectionFactory
     *         lettuce 连接工厂
     *
     * @return {@link RedisTemplate}
     */
    @Bean
    public RedisTemplate <String, Object> redisTemplate (LettuceConnectionFactory lettuceConnectionFactory) {
        // 设置序列化
        Jackson2JsonRedisSerializer <Object> jackson2JsonRedisSerializer = this.getJackson2JsonRedisSerializer();

        // 配置redisTemplate
        RedisTemplate <String, Object> redisTemplate = new RedisTemplate <>();

        redisTemplate.setConnectionFactory(lettuceConnectionFactory);

        RedisSerializer <String> stringRedisSerializer = this.keySerializer();
        // key序列化
        redisTemplate.setKeySerializer(stringRedisSerializer);
        // value序列化
        redisTemplate.setValueSerializer(jackson2JsonRedisSerializer);
        // Hash key序列化
        redisTemplate.setHashKeySerializer(stringRedisSerializer);
        // Hash value序列化
        redisTemplate.setHashValueSerializer(jackson2JsonRedisSerializer);

        redisTemplate.afterPropertiesSet();

        return redisTemplate;
    }

    /**
     * 对hash类型操作
     *
     * @param redisTemplate
     *         {@link org.springframework.data.redis.core.RedisTemplate}
     *
     * @return {@link HashOperations}
     */
    @Bean
    public HashOperations <String, String, Object> hashOperations (RedisTemplate <String, Object> redisTemplate) {
        return redisTemplate.opsForHash();
    }

    /**
     * @param redisTemplate
     *         {@link org.springframework.data.redis.core.RedisTemplate}
     *
     * @return {@link ValueOperations}
     */
    @Bean
    public ValueOperations <String, Object> valueOperations (RedisTemplate <String, Object> redisTemplate) {
        return redisTemplate.opsForValue();
    }

    /**
     * @param redisTemplate
     *         {@link org.springframework.data.redis.core.RedisTemplate}
     *
     * @return {@link ListOperations}
     */
    @Bean
    public ListOperations <String, Object> listOperations (RedisTemplate <String, Object> redisTemplate) {
        return redisTemplate.opsForList();
    }

    /**
     * @param redisTemplate
     *         {@link org.springframework.data.redis.core.RedisTemplate}
     *
     * @return {@link SetOperations}
     */
    @Bean
    public SetOperations <String, Object> setOperations (RedisTemplate <String, Object> redisTemplate) {
        return redisTemplate.opsForSet();
    }

    /**
     * @param redisTemplate
     *         {@link org.springframework.data.redis.core.RedisTemplate}
     *
     * @return {@link ZSetOperations}
     */
    @Bean
    public ZSetOperations <String, Object> zSetOperations (RedisTemplate <String, Object> redisTemplate) {
        return redisTemplate.opsForZSet();
    }

    /**
     * 缓存失败handler
     *
     * @return {@link CacheErrorHandler}
     */
    @Override
    public CacheErrorHandler errorHandler () {
        // 异常处理，当Redis发生异常时，打印日志，但是程序正常走
        log.info("初始化 -> [{}]", "Redis CacheErrorHandler");

        return new CacheErrorHandler() {
            @Override
            public void handleCacheGetError (RuntimeException e, Cache cache, Object key) {
                log.error("Redis：获取数据失败：key -> [{}]", key, e);
            }

            @Override
            public void handleCachePutError (RuntimeException e, Cache cache, Object key, Object value) {
                log.error("Redis：数据加入缓存失败：key -> [{}]；value -> [{}]", key, value, e);
            }

            @Override
            public void handleCacheEvictError (RuntimeException e, Cache cache, Object key) {
                log.error("Redis：删除Key失败：key -> [{}]", key, e);
            }

            @Override
            public void handleCacheClearError (RuntimeException e, Cache cache) {
                log.error("Redis：清空缓存失败：", e);
            }
        };
    }

    /**
     * 获取缓存配置
     *
     * @return 缓存配置
     */
    private Map <String, RedisCacheConfiguration> getRedisCacheConfigurationMap () {
        Map <String, Integer> cacheItems = MapUtils.emptyIfNull(this.cacheProperties.getItems());

        Map <String, RedisCacheConfiguration> redisCacheConfigurationMap = new HashMap <>();

        for (Map.Entry <String, Integer> entry : cacheItems.entrySet()) {
            redisCacheConfigurationMap.put(entry.getKey(), this.getRedisCacheConfigurationWithTtl(entry.getValue()));
        }

        return redisCacheConfigurationMap;
    }

    /**
     * 获取redis的配置
     *
     * @param seconds
     *         过期时间，单位秒
     *
     * @return {@link org.springframework.data.redis.cache.RedisCacheConfiguration}
     */
    private RedisCacheConfiguration getRedisCacheConfigurationWithTtl (Integer seconds) {

        Jackson2JsonRedisSerializer <Object> jsonRedisSerializer = this.getJackson2JsonRedisSerializer();

        return RedisCacheConfiguration.defaultCacheConfig()
                .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(jsonRedisSerializer))
                .entryTtl(Duration.ofSeconds(seconds));
    }

    /**
     * key 序列化
     *
     * @return {@link RedisSerializer}
     */
    private RedisSerializer <String> keySerializer () {
        return new StringRedisSerializer();
    }

    /**
     * 值序列化
     *
     * @return {@link RedisSerializer}
     */
    private RedisSerializer <Object> valueSerializer () {
        // 设置序列化
        return this.getJackson2JsonRedisSerializer();
    }

    /**
     * 使用jackson
     *
     * @return {@link Jackson2JsonRedisSerializer}
     */
    private Jackson2JsonRedisSerializer <Object> getJackson2JsonRedisSerializer () {
        // 用于解决无法序列化问题
        ObjectMapper om = new ObjectMapper().disable(SerializationFeature.FAIL_ON_EMPTY_BEANS);

        // 设置为默认时区，
        om.setTimeZone(TimeZone.getTimeZone("GMT+8"));

        // 禁止转换为时间戳
        om.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
        // 注册时间模块
        om.registerModule(new JavaTimeModule());

        om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        // 指定要序列化的域，类必须是非final修饰的，比如String,Long等会抛出异常，因为这些类都是final的
        // 关闭该功能有可能会引起序列化的问题，如果出现问题，请开启该选项，另外应该修改其他序列化的代码
        om.activateDefaultTyping(LaissezFaireSubTypeValidator.instance, ObjectMapper.DefaultTyping.NON_FINAL);

        om.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
        om.setSerializationInclusion(JsonInclude.Include.NON_NULL);

        //        this.addFilter(om);

        return new Jackson2JsonRedisSerializer <>(om, Object.class);
    }

    // ========================== 系统不应缓存Hibernate 查询出来的实体类， 而应缓存对应的DTO，如果缓存DTO，那么以下配置可忽略

    /**
     * jackson 序列化的时候，可能会序列化Hibernate Proxy对象（这个对象是怎么产生的，目前还不清楚），所以现在过滤掉部分属性
     */
    public static final String[] HIBERNATE_PROXY_PROPERTIES = {"hibernateLazyInitializer", "handler", "fieldHandler", "$$_hibernate_interceptor"};

    /**
     * 添加过滤器。在要序列化的实体类上增加 @JsonFilter ("hibernateProxyFilter") 启用过滤器。用在BaseEntity上，这样就相当于全部实体类都启用该配置
     * <p>
     * 但因在BaseEntity上增加@JsonFitler会影响到原来的结果，此时需要更改jackson的配置方式或者直接使用@JsonIgnoreProperties
     *
     * @param om
     */
    private void addFilter (ObjectMapper om) {
        FilterProvider filterProvider = new SimpleFilterProvider().addFilter("hibernateProxyFilter",
                SimpleBeanPropertyFilter.serializeAllExcept(HIBERNATE_PROXY_PROPERTIES)
        );
        om.setFilterProvider(filterProvider);
    }
}
